在先前我們所撰寫的單元測試中,3A 原則所做的不外乎是新增物件、執行物件方法、驗證物件回傳的結果或呼叫物件本身的屬性。好,關鍵在最後驗證的時候,那如果今天他方法執行最後的不是回傳值或修改物件本身的屬性;而是呼叫另一個物件的方法呢?
相信看到這邊還是有點抽象,那我們先來看一段程式碼
using LLLog;
public class LogSystem
{
public LogSystem()
{
}
public void LogFunction(string LogMessage)
{
var LogService = new LLLogService();
LogService.Log(LogMessage);
}
}
我們從這段程式碼可以看到執行 LogFunction 的時候,該方法最後呼叫 Log 方法時,並沒有任何回傳值,若用以前回傳資料的驗證方式,在此情況則無法處理。針對此情況,Roy Osherove 在單元測試的藝術中提出了互動測試 (Interaction Testing)的概念,其定義如下:
互動測試是針對一個物件如何向其他的物件發送訊息(呼叫方法)的測試。如果一個特定的工作單元的最終結果,是呼叫另外一個物件,你就需要進行互動測試。
相信如果第一次看段文字的人,應該很多人跟我一樣一頭霧水。
但他在後來文中有提到一個例子還滿清楚的解釋互動測試的感覺,在這邊寫給各位做參考。假設今天有個灌溉系統,這個灌溉系統的目的是要給樹澆水,每十二個小時澆一次水,我們要驗證灌溉系統是否符合我們預期的結果有兩種方式驗證方式:
(1) 基於狀態的整合測試:直接檢查樹的情況,這樣的好處就是可以直接知道其狀況,不會有測試結果與真實情況不符。但是其流程繁雜,且容易因為樹的品種、環境等等,需要 Case by Case 設計。
(2) 互動測試:我們在澆水的水管末端,裝上感測器,檢測是否有在預期的時間澆水。換言之,我們不管樹的情況,而是管「有沒有澆水」,這樣的好處就不會受樹的品種、環境等等影響。
因此,接下來就是要探討如何在撰寫單元測試的時候如何把最後呼叫第三方套件的方法抽換成模擬物件,以下兩張圖分別是以前做資料驗證的方式及今天接下來要討論的驗證。
圖 1、資料測試的流程
圖 2、互動測試的流程
那接下來一樣也用故事情境一步一步加深開發的情境與難度。
今天開發者接到新的需求,要開始撰寫一隻 Log 紀錄系統,在法呼叫這隻方法時,就會登記 Log 在相對應檔案的記事本上,而先前已有前輩開發了一隻套件叫 LLLog,所以他所需要做的事情也就是撰寫好相對應的商業邏輯,如下:
介面設計:
public class LogSystem
{
private ILogService LogService;
public LogSystem(ILogService inLogService)
{
LogService = inLogService;
}
public void LogFunction(string LogMessage)
{
LogService.Log(LogMessage);
}
}
介面實作:
using LLLog;
public class DemoLLLogSerivce : ILogService
{
var LogService = new LLLogService();
public string Log(string LogMessage)
{
LogService.Log(LogMessage);
}
}
而針對測試的戰術,因我們是要取代 LLLog 的 LogService.Log 方法;而該方法是沒有回傳值,但我們在測試時要有相對應的回傳值,因此我們在模擬物件的類別中設定了屬性,跑 Log 方法時把 Message 記錄在 MockLogSerivce 的 logMessage,程式碼如下:
using NUnit3;
[TestFixture]
public class LogSystemUnitTests
{
[Test]
public void LogFunction_Success()
{
// Arrange
MockLogSerivce mockLogService = new MockLogSerivce();
LogSystem LogService = new LogSystem(mockLogService);
// Act
LogService.Log("Test Demo");
// Assert
Assert.AreEqual("Test Demo", mockLogService.logMessage);
}
}
public class MockLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
如此以來,就解決沒有回傳值而無法驗證的問題。
我們從上述的程式碼中,簡單詮釋了模擬物件的概念;然而,什麼時候該用虛設常式,什麼時候要用模擬物件,兩者搭配起來又有什麼樣的情況,兩者之間都出現在測試裡面又要怎麼區分,都會在明天的文章中做個整理。